/*
 * Copyright 2007-2009 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.paoding.rose.web.impl.thread;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.servlet.ServletContext;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import net.paoding.rose.Rc;
import net.paoding.rose.web.Invocation;
import net.paoding.rose.web.annotation.Param;
import net.paoding.rose.web.impl.typeconverter.PaodingActionsTypeConverter;
import net.paoding.rose.web.var.Flash;
import net.paoding.rose.web.var.Model;
import net.paoding.rose.web.var.PrivateVar;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.TypeMismatchException;
import org.springframework.util.ClassUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartRequest;
import org.springframework.web.util.WebUtils;

public class MethodParameterResolver {
	private static Log logger = LogFactory.getLog(MethodParameterResolver.class);

	public static final String MAP_SEPARATOR = ":";
	private static PaodingActionsTypeConverter converter = new PaodingActionsTypeConverter();

	private ActionEngine action;
	private Class<?>[] parameterTypes;

	public MethodParameterResolver(ActionEngine engine) {
		this.action = engine;
		parameterTypes = engine.getMethod().getParameterTypes();

	}

	public boolean hasMultipartParams() {
		for (Class<?> parameterType : parameterTypes) {
			// MultipartRequest or MultipartFile
			if (MultipartRequest.class == parameterType || MultipartFile.class == parameterType) {
				return true;
			}
		}
		return false;
	}

	public void resolve(HttpServletRequest request, Object[] parameters)
			throws InstantiationException, IllegalAccessException, SecurityException,
			NoSuchMethodException {
		String[] names = action.getParameterNames();
		Map<String, Object> beans = new HashMap<String, Object>(2);
		BindingResult parameterBindingResult = Rc.getParameterBindingResult();
		for (int i = 0; i < parameters.length; i++) {
			try {
				bindParameter(request, names, parameters, i, beans, parameterBindingResult);
			} catch (Exception ex) {
				parameterBindingResult.rejectValue(names[i], "exception", new Object[] { names[i],
						ex }, "param [" + names[i] + "] bind failed:" + ex.getMessage());
			}
		}
		ServletRequestDataBinder binder;
		for (String name : beans.keySet()) {
			Object bean = beans.get(name);
			// apply binder to custom target object
			if (beans.size() == 1) {
				binder = new ServletRequestDataBinder(bean);
			} else {
				binder = new ServletRequestDataBinder(bean, name);
			}
			binder.bind(request);
			Rc.model().add(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
		}
		for (int i = 0; i < parameters.length; i++) {
			if (parameterTypes[i] == BindingResult.class || parameterTypes[i] == Errors.class) {
				if (Rc.getBindingResult(names[i]) != null) {
					parameters[i] = Rc.getBindingResult(names[i]);
				} else {
					if (i == 0) {
						parameters[i] = Rc.getParameterBindingResult();
					} else if (parameters[i - 1] != null
							&& Rc.getBindingResult(parameters[i - 1]) != null) {
						parameters[i] = Rc.getBindingResult(parameters[i - 1]);
					} else {
						parameters[i] = Rc.getParameterBindingResult();
					}
				}
			}
		}
	}

	private void bindParameter(HttpServletRequest request, String[] names, Object[] parameters,
			int i, Map<String, Object> beans, BindingResult parameterBindingResult) {
		Class<?> parameterType = parameterTypes[i];
		// Model
		if (Model.class == parameterType) {
			parameters[i] = Rc.model();
		}
		// Invocation
		if (Invocation.class == parameterType) {
			parameters[i] = PrivateVar.getInvocation();
		}
		// Flash
		else if (Flash.class == parameterType) {
			parameters[i] = Rc.flash();
		}
		// Map
		else if (Map.class == parameterType) {
			if (StringUtils.isNotBlank(names[i])) {
				parameters[i] = WebUtils.getParametersStartingWith(request, names[i]
						+ MAP_SEPARATOR);
			} else {
				parameters[i] = Collections.EMPTY_MAP;
			}
		}
		// List
		else if (List.class == parameterType || ArrayList.class == parameterType
				|| Collection.class == parameterType) {
			if (StringUtils.isNotBlank(names[i])) {
				String[] values = request.getParameterValues(names[i]);
				ArrayList<String> param = new ArrayList<String>();
				for (String value : values) {
					param.add(value);
				}
				parameters[i] = param;
			} else {
				parameters[i] = Collections.EMPTY_LIST;
			}
		}
		// Set
		else if (Set.class == parameterType || HashSet.class == parameterType) {
			if (StringUtils.isNotBlank(names[i])) {
				String[] values = request.getParameterValues(names[i]);
				HashSet<String> param = new HashSet<String>();
				for (String value : values) {
					param.add(value);
				}
				parameters[i] = param;
			} else {
				parameters[i] = Collections.EMPTY_SET;
			}
		}
		// Array
		else if (parameterType.isArray()) {
			if (names[i] != null) {
				Object toConvert = request.getParameterValues(names[i]);
				if (((String[]) toConvert).length == 1) {
					toConvert = ((String[]) toConvert)[0];
				}
				parameters[i] = converter.convertIfNecessary(toConvert, parameterType);
			} else {
				parameters[i] = new Object[0];
			}
		}
		// int, Integer, String, ..., Map, Collection, Set, List, ...
		// 将path的param按规则设置到method中
		// 放到Map,Set等下面。。以免这些被converter处理(converter的处理方式不是符合我们通常惯例的)
		else if (ClassUtils.isPrimitiveOrWrapper(parameterType) || parameterType == String.class
				|| converter.findCustomEditor(parameterType, null) != null
				|| converter.getDefaultEditor(parameterType) != null) {
			MatchResult controllerMatchResult = PrivateVar.getInvocation().getControllerMatchResult();
			MatchResult actionMatchResult = PrivateVar.getInvocation().getActionMatchResult();
			String toConvert;
			if (names[i] != null) {
				toConvert = actionMatchResult.getParameter(names[i]);
				if (toConvert == null) {
					toConvert = controllerMatchResult.getParameter(names[i]);
				}
				if (toConvert == null) {
					toConvert = request.getParameter(names[i]);
				}
			} else {
				toConvert = actionMatchResult.getParameter(i);
				if (toConvert == null) {
					toConvert = controllerMatchResult.getParameter(names[i]);
				}
			}
			if (toConvert == null) {
				Param param = action.getParamAnnocationAt(i);
				if (param != null && !"~".equals(param.def())) {
					if (parameterType.isPrimitive()) {
						parameters[i] = converter.convertIfNecessary(param.def(), parameterType);
					}
				}
			} else {
				if (parameterType == String.class) {
					parameters[i] = toConvert;
				} else {
					try {
						parameters[i] = converter.convertIfNecessary(toConvert, parameterType);
					} catch (TypeMismatchException e) {
						parameterBindingResult.rejectValue(names[i], "convert.failed",
								new Object[] { names[i], e }, e.getMessage());
						if (parameterType.isPrimitive()) {
							Param param = action.getParamAnnocationAt(i);
							if (param != null && !"~".equals(param.def())) {
								parameters[i] = converter.convertIfNecessary(param.def(),
										parameterType);
							} else {
								parameters[i] = converter.convertIfNecessary("0", parameterType);
							}
						}
					}
				}
			}
		}
		// ServletRequest
		else if (HttpServletRequest.class == parameterType || ServletRequest.class == parameterType) {
			parameters[i] = Rc.request();
		}
		// MultipartRequest
		else if (MultipartRequest.class == parameterType) {
			if (Rc.request() instanceof MultipartRequest) {
				parameters[i] = Rc.request();
			} else {
				if (logger.isDebugEnabled()) {
					logger.debug("cann't set" + " MultipartRequest param to method "
							+ this.action.getMethod().getName()
							+ ", the request is not a MultipartRequest");
				}
			}
		}
		// MultipartFile
		else if (MultipartFile.class == parameterType) {
			if (Rc.request() instanceof MultipartRequest) {
				MultipartRequest multipartRequest = (MultipartRequest) Rc.request();
				parameters[i] = multipartRequest.getFile(names[i]);
				if (parameters[i] == null) {
					if (logger.isDebugEnabled()) {
						logger.debug("not found MultipartFile named:" + names[i]);
					}
				}
			} else {
				if (logger.isDebugEnabled()) {
					logger.debug("cann't " + "set MultipartFile param to method "
							+ this.action.getMethod().getName()
							+ ", the request is not a MultipartRequest");
				}
			}
		}
		// HttpSession
		else if (HttpSession.class == parameterType) {
			parameters[i] = Rc.session();
		}
		// ServletResponse
		else if (HttpServletResponse.class == parameterType
				|| ServletResponse.class == parameterType) {
			parameters[i] = Rc.response();
		}
		// ServletContext
		else if (ServletContext.class == parameterType) {
			parameters[i] = Rc.getServletContext();
		} else if (!parameterType.isInterface()
				&& !Modifier.isAbstract(parameterType.getModifiers())) {
			parameters[i] = BeanUtils.instantiateClass(parameterType);
			beans.put(names[i], parameters[i]);
		}
		if (parameters[i] == null) {
			if (BindingResult.class != parameterType && Errors.class != parameterType) {
				if (parameterType.isPrimitive()) {
					parameters[i] = converter.convertIfNecessary("0", parameterType);
				}
				Param param = action.getParamAnnocationAt(i);
				if (param != null && param.required()) {
					parameterBindingResult.rejectValue(names[i], "required",
							new Object[] { names[i] }, "param [" + names[i] + "] is required.");
				}
			}
		}
	}

	public boolean isSimpleParameter(Class<?> parameterType) {
		return ClassUtils.isPrimitiveOrWrapper(parameterType) || parameterType == String.class
				|| Collection.class.isAssignableFrom(parameterType) || parameterType.isArray();
		// return parameterType.isPrimitive()
		// || parameterType.getName().startsWith("java");
	}
}
